﻿namespace OverStore.CompositeIdentifiers
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using OverStore.Runtime.Storage;

    /// <summary>
    /// Contains extension methods for working with multipart object identifiers.
    /// </summary>
    public static class KeyHelper
    {
        /// <summary>
        /// Wraps specifed result to <see cref="KeyValuePair{TKey,TValue}"/> with specified name.
        /// </summary>
        /// <typeparam name="TValue">The type of the result.</typeparam>
        /// <param name="value">The result.</param>
        /// <param name="name">The name.</param>
        public static KeyValuePair<string, TValue> As<TValue>(this TValue value, string name)
        {
            return new KeyValuePair<string, TValue>(name, value);
        }

        /// <summary>
        /// Creates new instance of <see cref="Key2{TFirst,TSecond}"/> with specified arguments.
        /// </summary>
        /// <typeparam name="TFirst">The type of the first result.</typeparam>
        /// <typeparam name="TSecond">The type of the second result.</typeparam>
        /// <param name="first">The first result.</param>
        /// <param name="second">The second result.</param>
        /// <returns>Key contains specified names and values.</returns>
        public static Key2<TFirst, TSecond> MultipartKey<TFirst, TSecond>(KeyValuePair<string, TFirst> first, KeyValuePair<string, TSecond> second)
        {
            return new Key2<TFirst, TSecond>(first, second);
        }

        /// <summary>
        /// Creates new instance of <see cref="Key3{TFirst,TSecond,TThird}"/> with specified arguments.
        /// </summary>
        /// <typeparam name="TFirst">The type of the first result.</typeparam>
        /// <typeparam name="TSecond">The type of the second result.</typeparam>
        /// <typeparam name="TThird">The type of the third result.</typeparam>
        /// <param name="first">The first result.</param>
        /// <param name="second">The second result.</param>
        /// <param name="third">The third result.</param>
        /// <returns>Key contains specified names and values.</returns>
        public static Key3<TFirst, TSecond, TThird> MultipartKey<TFirst, TSecond, TThird>(KeyValuePair<string, TFirst> first, KeyValuePair<string, TSecond> second, KeyValuePair<string, TThird> third)
        {
            return new Key3<TFirst, TSecond, TThird>(first, second, third);
        }
    }

    /// <summary>
    /// Immutable composite key consist of two elements.
    /// </summary>
    public class Key2<TFirst, TSecond> : IRecord
    {
        /// <summary>
        /// Initializes new instance of Key2 class.
        /// </summary>
        public Key2(KeyValuePair<string, TFirst> firstPart, KeyValuePair<string, TSecond> secondPart) :
            this(firstPart, secondPart, false)
        {
        }

        /// <summary>
        /// Initializes new instance of Key2 class.
        /// </summary>
        protected Key2(KeyValuePair<string, TFirst> firstPart, KeyValuePair<string, TSecond> secondPart, bool skipUniquenessCheck)
            : this()
        {
            if (!skipUniquenessCheck)
                CheckUniquenessAndNulls(firstPart.Key, secondPart.Key);
            FirstName = firstPart.Key;
            First = firstPart.Value;

            SecondName = secondPart.Key;
            Second = secondPart.Value;
        }

        /// <summary>
        /// Initializes new instance of Key2 class.
        /// </summary>
        protected Key2()
        {
        }

        /// <summary>
        /// Get name of first part of key.
        /// </summary>
        public string FirstName { get; protected set; }

        /// <summary>
        /// Get result of first part of key.
        /// </summary>
        public TFirst First { get; protected set; }

        /// <summary>
        /// Get name of second part of key.
        /// </summary>
        public string SecondName { get; protected set; }

        /// <summary>
        /// Get result of second part of key.
        /// </summary>
        public TSecond Second { get; protected set; }

        /// <summary>
        /// Get result of key part with specified name. If result found, method returns true and
        /// return found result in <paramref name="result"/> parameter.
        /// </summary>
        /// <param name="name">The name of result.</param>
        /// <param name="result">The result with specified name.</param>
        /// <returns>True if result with specified name exists, false otherwise.</returns>
        /// <remarks>
        /// DefaultReadVariantConfiguration implementation compares each available property with specified name in most simple
        /// manner. See example below
        /// <code>
        /// protected virtual bool TryGetValue(string name, out object result)
        /// {
        /// if (StringComparer.OrdinalIgnoreCase.Equals(name, FirstName))
        /// {
        /// result = First;
        /// return true;
        /// }
        /// if (StringComparer.OrdinalIgnoreCase.Equals(name, SecondName))
        /// {
        /// result = Second;
        /// return true;
        /// }
        /// result = null;
        /// return false;
        /// }
        /// </code>
        /// </remarks>
        public virtual bool TryGetValue(string name, out object result)
        {
            if (StringComparer.OrdinalIgnoreCase.Equals(name, FirstName))
            {
                result = First;
                return true;
            }

            if (StringComparer.OrdinalIgnoreCase.Equals(name, SecondName))
            {
                result = Second;
                return true;
            }

            result = null;
            return false;
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            var nameValuePairs = new[]
                         {
                             String.Format(CultureInfo.InvariantCulture, "{0} = {1}", FirstName, First),
                             String.Format(CultureInfo.InvariantCulture, "{0} = {1}", SecondName, Second)
                         };
            return String.Join(",", nameValuePairs);
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        ///     <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">
        /// The <paramref name="obj"/> parameter is null.
        /// </exception>
        public override bool Equals(object obj)
        {
            var key = obj as Key2<TFirst, TSecond>;
            if (ReferenceEquals(key, null))
                return base.Equals(obj);
            return StringComparer.OrdinalIgnoreCase.Equals(FirstName, key.FirstName) &&
                   EqualityComparer<TFirst>.Default.Equals(First, key.First) &&
                   StringComparer.OrdinalIgnoreCase.Equals(SecondName, key.SecondName) &&
                   EqualityComparer<TSecond>.Default.Equals(Second, key.Second);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode()
        {
            unchecked
            {
                return StringComparer.OrdinalIgnoreCase.GetHashCode(FirstName) +
                       EqualityComparer<TFirst>.Default.GetHashCode(First) +
                       StringComparer.OrdinalIgnoreCase.GetHashCode(SecondName) +
                       EqualityComparer<TSecond>.Default.GetHashCode(Second);
            }
        }

        /// <summary>
        /// Creates the copy of key with same names but with new values.
        /// </summary>
        /// <param name="first">The first result.</param>
        /// <param name="second">The second result.</param>
        /// <returns>New key with same name but specified values.</returns>
        public Key2<TFirst, TSecond> CreateCopy(TFirst first, TSecond second)
        {
            return new Key2<TFirst, TSecond>
                       {
                           FirstName = FirstName,
                           First = first,
                           SecondName = SecondName,
                           Second = second
                       };
        }

        /// <summary>
        /// Checks specified names to be different and does not contains null or empty strings.
        /// </summary>
        /// <param name="names">The names to check.</param>
        protected static void CheckUniquenessAndNulls(params string[] names)
        {
            for (int i = 0; i < names.Length; i++)
            {
                if (String.IsNullOrEmpty(names[i]))
                    throw new ArgumentNullException(
                        String.Format(CultureInfo.InvariantCulture, "Name at index {0} is null or empty", i));
                for (int j = i + 1; j < names.Length; j++)
                    if (StringComparer.OrdinalIgnoreCase.Equals(names[i], names[j]))
                        throw new ArgumentException(
                            String.Format(CultureInfo.InvariantCulture, "Names at index {0} and {1} are equal [{2}]", i, j, names[i]));
            }
        }
    }

    /// <summary>
    /// Immutable composite key consist of three elements.
    /// </summary>
    public class Key3<TFirst, TSecond, TThird> : Key2<TFirst, TSecond>
    {
        /// <summary>
        /// Initializes new instance of Key3 class.
        /// </summary>
        public Key3(KeyValuePair<string, TFirst> firstPart, KeyValuePair<string, TSecond> secondPart, KeyValuePair<string, TThird> thirdPart)
            : this(firstPart, secondPart, thirdPart, false)
        {
        }

        /// <summary>
        /// Initializes new instance of Key3 class.
        /// </summary>
        protected Key3(KeyValuePair<string, TFirst> firstPart, KeyValuePair<string, TSecond> secondPart, KeyValuePair<string, TThird> thirdPart, bool skipUniquenessCheck)
            : base(firstPart, secondPart, true)
        {
            if (!skipUniquenessCheck)
                CheckUniquenessAndNulls(firstPart.Key, secondPart.Key, thirdPart.Key);

            ThirdName = thirdPart.Key;
            Third = thirdPart.Value;
        }

        /// <summary>
        /// Initializes new instance of Key3 class.
        /// </summary>
        protected Key3()
        {
        }

        /// <summary>
        /// Get name of third part of key
        /// </summary>
        public string ThirdName { get; protected set; }

        /// <summary>
        /// Get result of third part of key
        /// </summary>
        public TThird Third { get; protected set; }

        /// <summary>
        /// Get result of key part with specified name. If result found, method returns true and
        /// return found result in <paramref name="result"/> parameter.
        /// </summary>
        /// <param name="name">The name of result.</param>
        /// <param name="result">The result with specified name.</param>
        /// <remarks>
        /// DefaultReadVariantConfiguration implementation compares each available property with specified name in most simple
        /// manner. See example below
        /// <code>
        /// protected virtual bool TryGetValue(string name, out object result)
        /// {
        /// if (StringComparer.OrdinalIgnoreCase.Equals(name, FirstName))
        /// {
        /// result = First;
        /// return true;
        /// }
        /// if (StringComparer.OrdinalIgnoreCase.Equals(name, SecondName))
        /// {
        /// result = Second;
        /// return true;
        /// }
        /// result = null;
        /// return false;
        /// }
        /// </code>
        /// </remarks>
        public override bool TryGetValue(string name, out object result)
        {
            if (StringComparer.OrdinalIgnoreCase.Equals(name, ThirdName))
            {
                result = Third;
                return true;
            }

            return base.TryGetValue(name, out result);
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        ///     <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">
        /// The <paramref name="obj"/> parameter is null.
        /// </exception>
        public override bool Equals(object obj)
        {
            var key3 = obj as Key3<TFirst, TSecond, TThird>;
            if (ReferenceEquals(key3, null))
                return base.Equals(obj);
            return base.Equals(key3) &&
                   StringComparer.OrdinalIgnoreCase.Equals(ThirdName, key3.ThirdName) &&
                   EqualityComparer<TThird>.Default.Equals(Third, key3.Third);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode()
        {
            unchecked
            {
                return base.GetHashCode() + StringComparer.OrdinalIgnoreCase.GetHashCode(ThirdName) +
                       EqualityComparer<TThird>.Default.GetHashCode(Third);
            }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return String.Join(",", new[] { base.ToString(), String.Format(CultureInfo.InvariantCulture, "{0} = {1}", ThirdName, Third) });
        }

        /// <summary>
        /// Creates the copy of current instance with same names but with new values.
        /// </summary>
        /// <param name="first">The first result.</param>
        /// <param name="second">The second result.</param>
        /// <param name="third">The third result.</param>
        /// <returns>The key with same name but specified values.</returns>
        public Key3<TFirst, TSecond, TThird> CreateCopy(TFirst first, TSecond second, TThird third)
        {
            return new Key3<TFirst, TSecond, TThird>
                       {
                           FirstName = FirstName,
                           First = first,
                           SecondName = SecondName,
                           Second = second,
                           ThirdName = ThirdName,
                           Third = third
                       };
        }
    }
}